﻿/*
BF3Rcon.NET, provides a .NET implementation of BF3's RCON interface.
Copyright (C) 2011 agentwite, Timi, Unseen, AlCapwn

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3rcon.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Collections.ObjectModel;
using System.Threading;
using System.Globalization;

//TODO: consider rewriting this class to better support inheritance
///// <summary>
///// Provides the Battlefield 3 RCON client and the other classes it uses.
///// </summary>
namespace System.Net.Battlefield3
{
    /// <summary>
    /// Provides the socket's state object for asynchronous sockets
    /// </summary>
    class ReadState
    {
        internal RconClient Client;
        internal byte[] Buffer = new byte[RconClient.BufferSize];

        internal ReadState(RconClient client)
        {
            this.Client = client;
        }
    }

    /// <summary>
    /// Provides the client's state object for parsing packets in a <see cref="ThreadPool"/> thread.
    /// </summary>
    class ParsePacketState
    {
        internal RconClient Client;
        internal Packet Packet;

        internal ParsePacketState(RconClient client, Packet packet)
        {
            Client = client;
            Packet = packet;
        }
    }

    /// <summary>
    /// Provides an implementation of the Battlefield 3 RCON protocol.
    /// </summary>
    public class RconClient :
        IDisposable
    {
        internal const int BufferSize = 4096;

        //this is used for the tostrings to satisfy fxcop
        static CultureInfo c = CultureInfo.GetCultureInfo("en-US");

        /// <summary>
        /// The maximum amount of time the <see cref="LogOn(string, bool)"/> method will take before returning false
        /// when the thread is being blocked.
        /// <para />
        /// See Remarks.
        /// </summary>
        /// <remarks>
        /// Two communications must be made to logon, so the maximum total time will
        /// be less than <see cref="LogOnTimeout"/> * 2.
        /// <seealso cref="LogOn(string, bool)"/>
        /// <seealso cref="LoggedOn"/>
        /// </remarks>
        public const int LogOnTimeout = 15;

        internal List<byte> LeftoverData = new List<byte>(BufferSize);

        Socket underlyingSocket;
        //Thread packetParsingThread = new Thread(new ThreadStart());
        //Queue<Packet> packetQueue = new Queue<Packet>(10); // random number

        //this is used to determine what the server is replying to by
        //tracing it's sequence back to the sequence the <see cref="RconClient"/> used
        Dictionary<int, string> sequenceTracker = new Dictionary<int, string>();

        //this list tracks which sequences are sent internally
        //the number of elements should remain low, so O(n) for removes isnt much of an issue
        //however, for efficiency, this will not be used if suppressinternalcommands is false
        List<int> internalSequences = new List<int>(10);

        //this will be incremented to provide a unique sequence each time the <see cref="RconClient"/> sends data (not responds)
        int clientSequence = 0;

        Timer playerRefreshTimer;
        string playerRefreshCommand; //rather than change the timer on logon, just change the command used

        #region Ctors
        /// <summary>
        /// Creates a new <see cref="RconClient"/> for remotely communicating with a Battlefield 3 server.
        /// </summary>
        public RconClient()
        {
            SuppressInternalCommands = true;
        }
        #endregion

        #region Properties
        // a tcp client is used because it's connect is sometimes faster than a regular socket's, and this is more flexible
        TcpClient _Socket = new TcpClient();

        TcpClient Socket { get { return _Socket; } }

        /// <summary>
        /// Gets or sets the address that this client will <see cref="Connect()"/> to.
        /// </summary>
        /// <value>The address of the server.</value>
        /// <remarks>In order for changes of the <see cref="Address"/> and <see cref="Port"/> to take effect, you must
        /// <see cref="Disconnect"/> and <see cref="Connect()"/>.</remarks>
        public String Address { get; set; }

        int _Port;
        /// <summary>
        /// Gets or sets the port that this client will <see cref="Connect()"/> to.
        /// </summary>
        /// <value>The port of the server.</value>
        /// <remarks>In order for changes of the <see cref="Address"/> and <see cref="Port"/> to take effect, you must
        /// <see cref="Disconnect"/> and <see cref="Connect()"/>.</remarks>
        public int Port
        {
            get { return _Port; }
            set
            {
                if (value < 0 || value > 65535) throw new ArgumentOutOfRangeException("value", "The port must be between 0 and 65535.");
                _Port = value;
            }
        }

        /// <summary>
        /// Gets whether or not the <see cref="RconClient"/> is connected to the server.
        /// </summary>
        /// <value>True if the <see cref="RconClient"/> has connected to the server.</value>
        public bool IsConnected
        {
            get { return Socket.Connected; }
        }

        /// <summary>
        /// Gets whether or not the <see cref="RconClient"/> has logged into the server.
        /// </summary>
        /// <value>True if the <see cref="RconClient"/> has logged into the server.</value>
        public bool IsLoggedOn { get; private set; }

        PlayerCollection _Players = new PlayerCollection();
        /// <summary>
        /// Gets the <see cref="PlayerCollection"/> that contains all of the <see cref="Player">Players</see> on the server.
        /// </summary>
        /// <value>A <see cref="PlayerCollection"/> object containing the <see cref="Player">Players</see> in the server.</value>
        public PlayerCollection Players
        {
            get { return _Players; }
        }

        int _PlayerRefreshInterval = 5;
        /// <summary>
        /// Gets or sets the interval at which the <see cref="RconClient"/> will refresh the <see cref="PlayerCollection"/>.
        /// </summary>
        /// <value>The <see cref="PlayerCollection"/> refresh interval.</value>
        public int PlayerRefreshInterval
        {
            get { return _PlayerRefreshInterval; }
            set
            {
                if (value > (Int32.MaxValue / 1000)) throw new ArgumentOutOfRangeException(
                    "value", "value must be less than or equal to Int32.MaxValue / 1000.");
                _PlayerRefreshInterval = value;
                if (playerRefreshTimer != null) playerRefreshTimer.Change(0, value * 1000);
            }
        }

        /// <summary>
        /// Gets or sets whether or not the <see cref="Response"/> event will be raised for commands sent internally.
        /// </summary>
        /// <value>Whether or not the <see cref="Response"/> event will be raised for commands sent internally.</value>
        public bool SuppressInternalCommands { get; set; }
        #endregion

        #region Library Events
        /// <summary>
        /// Occurs when the <see cref="RconClient"/> receives a <see cref="Packet"/>
        /// </summary>
        public event EventHandler<RawReadEventArgs> RawRead;
        /// <summary>
        /// Raises the <see cref="RawRead"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="RawReadEventArgs"/> that contains the event data.</param>
        protected virtual void OnRawRead(RawReadEventArgs e)
        {
            if (RawRead != null) RawRead(this, e);
        }

        /// <summary>
        /// Occurs when the server responds to the <see cref="RconClient"/>.
        /// </summary>
        public event EventHandler<ResponseEventArgs> Response;
        /// <summary>
        /// Raises the <see cref="Response"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="ResponseEventArgs"/> that contains the event data.</param>
        protected virtual void OnResponse(ResponseEventArgs e)
        {
            if (Response != null) Response(this, e);
        }

        /// <summary>
        /// Occurs when the <see cref="RconClient"/> has connected to the server.
        /// </summary>
        public event EventHandler Connected;
        /// <summary>
        /// Raises the <see cref="Connected"/> event of the <see cref="RconClient"/>.
        /// </summary>
        protected virtual void OnConnected()
        {
            if (Connected != null) Connected(this, new EventArgs());
        }

        /// <summary>
        /// Occurs when the <see cref="RconClient"/> has encountered an error when trying to connect to the server.
        /// </summary>
        /// <remarks>
        /// This will only be raised when a SocketException occurs in the asynchronous connect method.
        /// </remarks>
        public event EventHandler<ConnectErrorEventArgs> ConnectError;
        /// <summary>
        /// Raises the <see cref="ConnectError"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="ConnectErrorEventArgs"/> that contains the event data.</param>
        protected virtual void OnConnectError(ConnectErrorEventArgs e)
        {
            if (ConnectError != null) ConnectError(this, e);
        }

        /// <summary>
        /// Occurs when the <see cref="RconClient"/> has disconnected from the server.
        /// </summary>
        public event EventHandler Disconnected;
        /// <summary>
        /// Raises the <see cref="Disconnected"/> event of the <see cref="RconClient"/>.
        /// </summary>
        protected virtual void OnDisconnected()
        {
            if (Disconnected != null) Disconnected(this, new EventArgs());
        }

        /// <summary>
        /// Occurs when the <see cref="RconClient"/> logs into the server.
        /// </summary>
        /// <remarks>The <see cref="RconClient"/> will have limited communication ability while not logged in.</remarks>
        public event EventHandler LoggedOn;
        /// <summary>
        /// Raises the <see cref="LoggedOn"/> event of the <see cref="RconClient"/>.
        /// </summary>
        protected virtual void OnLoggedOn()
        {
            if (LoggedOn != null) LoggedOn(this, new EventArgs());
        }
        #endregion

        #region RCON-originating Events
        /// <summary>
        /// Occurs when a <see cref="Player"/> leaves the server.
        /// </summary>
        public event EventHandler<PlayerEventArgs> PlayerLeft;
        /// <summary>
        /// Raises the <see cref="PlayerLeft"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="PlayerEventArgs"/> that contains the event data.</param>
        protected virtual void OnPlayerLeft(PlayerEventArgs e)
        {
            if (PlayerLeft != null) PlayerLeft(this, e);
        }

        /// <summary>
        /// Occurs when a <see cref="Player"/> joins the server.
        /// </summary>
        public event EventHandler<PlayerEventArgs> PlayerJoined;
        /// <summary>
        /// Raises the <see cref="PlayerJoined"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="PlayerEventArgs"/> that contains the event data.</param>
        protected virtual void OnPlayerJoined(PlayerEventArgs e)
        {
            if (PlayerJoined != null) PlayerJoined(this, e);
        }

        /// <summary>
        /// Occurs when a <see cref="Player"/> is authenticated.
        /// </summary>
        public event EventHandler<PlayerAuthenticatedEventArgs> PlayerAuthenticated;
        /// <summary>
        /// Raises the <see cref="PlayerAuthenticated"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="PlayerAuthenticatedEventArgs"/> that contains the event data.</param>
        protected virtual void OnPlayerAuthenticated(PlayerAuthenticatedEventArgs e)
        {
            if (PlayerAuthenticated != null) PlayerAuthenticated(this, e);
        }

        /// <summary>
        /// Occurs when a <see cref="Player"/> is killed.
        /// </summary>
        public event EventHandler<PlayerKilledEventArgs> PlayerKilled;
        /// <summary>
        /// Raises the <see cref="PlayerKilled"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="PlayerKilledEventArgs"/> that contains the event data.</param>
        protected virtual void OnPlayerKilled(PlayerKilledEventArgs e)
        {
            if (PlayerKilled != null) PlayerKilled(this, e);
        }

        /// <summary>
        /// Occurs when a <see cref="Player"/> spawns.
        /// </summary>
        public event EventHandler<PlayerEventArgs> PlayerSpawned;
        /// <summary>
        /// Raises the <see cref="PlayerSpawned"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="PlayerEventArgs"/> that contains the event data.</param>
        protected virtual void OnPlayerSpawned(PlayerEventArgs e)
        {
            if (PlayerSpawned != null) PlayerSpawned(this, e);
        }

        /// <summary>
        /// Occurs when a <see cref="Player"/> is moved to a new team, squad, or both.
        /// </summary>
        public event EventHandler<PlayerMovedEventArgs> PlayerMoved;
        /// <summary>
        /// Raises the <see cref="PlayerMoved"/> event of the <see cref="RconClient"/>.
        /// </summary>
        /// <param name="e">A <see cref="PlayerMovedEventArgs"/> that contains the event data.</param>
        protected virtual void OnPlayerMoved(PlayerMovedEventArgs e)
        {
            if (PlayerMoved != null) PlayerMoved(this, e);
        }

        /// <summary>
        /// Occurs when the round has ended.
        /// </summary>
        public event EventHandler RoundOver;
        /// <summary>
        /// Raises the <see cref="RoundOver"/> event of the <see cref="RconClient"/>.
        /// </summary>
        protected virtual void OnPlayerMoved()
        {
            if (RoundOver != null) RoundOver(this, new EventArgs());
        }
        #endregion

        #region Async Socket Methods
        static void Connect(RconClient client)
        {
            client.Socket.BeginConnect(client.Address, client.Port, ConnectCallback, client);
        }

        static void ConnectCallback(IAsyncResult ar)
        {
            RconClient client = (RconClient)ar.AsyncState;

            try
            {
                client.Socket.EndConnect(ar);
            }
            catch (SocketException ex)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(RaiseConnectErrorThread), new object[] { client, ex });
            }
            
            client.underlyingSocket = client.Socket.Client; //set the stream for more simple use by these methods

            //setup player refresh timer
            client.playerRefreshCommand = "listPlayers";
            client.playerRefreshTimer = new Timer(new TimerCallback(PlayerRefreshCallback), client, 0, client.PlayerRefreshInterval * 1000);

            //when the Connected event is raised, using LogOn (and future methods that use MRESes) blocks
            //subsequent Receives, and there's no easy way to determine if the MRESs will block the UI thread or
            //socket thread. therefore, this event will be raised in a ThreadPool thread.
            ThreadPool.QueueUserWorkItem(new WaitCallback(RaiseConnectedThread), client);

            Receive(client);
        }

        static void RaiseConnectedThread(object state)
        {
            (state as RconClient).OnConnected();
        }

        //state is an object array where 0 is rconclient and 1 is socketexception
        static void RaiseConnectErrorThread(object state)
        {
            object[] objs = state as object[];
            RconClient client = objs[0] as RconClient;
            SocketException ex = objs[1] as SocketException;

            client.OnConnectError(new ConnectErrorEventArgs(ex.Message, ex.SocketErrorCode));
        }

        static void Receive(RconClient client)
        {
            ReadState state = new ReadState(client);
            try
            {
                client.underlyingSocket.BeginReceive(state.Buffer, 0, state.Buffer.Length, 0, ReceiveCallback, state);
            }
            catch (InvalidOperationException) //socket is disposed (thru disconnect)
            {
                return;
            }
        }

        static void ReceiveCallback(IAsyncResult ar)
        {
            ReadState state = (ReadState)ar.AsyncState;
            RconClient client = state.Client;

            int bytesRead = client.underlyingSocket.EndReceive(ar);

            if (bytesRead > 0) //but I'll make use of it anyway
            {
                // if there is no leftover data from before and the buffer contains a complete packet, 
                // skip adding the buffer to leftoverData
                if (client.LeftoverData.Count == 0 && Packet.ContainsCompletePacket(state.Buffer, bytesRead))
                {
                    //create a list that will contain read data
                    List<byte> readBytes = new List<byte>(bytesRead);
                    for (int i = 0; i < bytesRead; i++)
                        readBytes.Add(state.Buffer[i]);

                    //add the new packet to the queue
                    //client.packetQueue.Enqueue(new Packet(readBytes, ref client.LeftoverData));

                    //parse the packet in a new thread
                    ThreadPool.QueueUserWorkItem(new WaitCallback(HandlePacketReceive),
                        new ParsePacketState(client, new Packet(readBytes, ref client.LeftoverData)));
                }
                else
                {
                    //add read bytes to leftoverdata
                    for (int i = 0; i < bytesRead; i++)
                        client.LeftoverData.Add(state.Buffer[i]);

                    while (Packet.ContainsCompletePacket(client.LeftoverData))
                    {
                        //add the new packet to the queue
                        //client.packetQueue.Enqueue(new Packet(client.LeftoverData, ref client.LeftoverData));

                        ThreadPool.QueueUserWorkItem(new WaitCallback(HandlePacketReceive),
                            new ParsePacketState(client, new Packet(client.LeftoverData, ref client.LeftoverData)));
                    }
                }
            }
            else
            {
                client.Disconnect();
            }

            Receive(client);
        }

        static void Send(RconClient client, Packet data)
        {
            client.underlyingSocket.BeginSend(data.ToByteArray(), 0, data.NumberOfBytes, 0, SendCallback, client);
        }

        static void SendCallback(IAsyncResult ar)
        {
            ((RconClient)ar.AsyncState).underlyingSocket.EndSend(ar);
        }
        #endregion

        #region Connect, Send
        /// <summary>
        /// Connects the <see cref="RconClient"/> to the server.
        /// </summary>
        public void Connect()
        {
            Connect(this);
        }

        /// <summary>
        /// Disconnects the <see cref="RconClient"/> from the server.
        /// </summary>
        public void Disconnect()
        {
            Socket.Close();
            _Socket = new TcpClient(); //prepare a new tcpclient for use

            //reset other properties specific to the last session
            LeftoverData = null;
            sequenceTracker = new Dictionary<int, string>();
            internalSequences = new List<int>(10);
            playerRefreshTimer.Dispose(); playerRefreshTimer = null;
            playerRefreshCommand = "listPlayers";

            OnDisconnected();
        }

        /// <summary>
        /// Sends a <see cref="Packet"/> to the server.
        /// </summary>
        /// <param name="packet">The <see cref="Packet"/> that is being sent.</param>
        protected void SendPacket(Packet packet)
        {
            if (!IsConnected) return; //consider throwing exception
            if (packet.Words.Count == 0) return;

            Send(this, packet);
        }

        /// <summary>
        /// Sends a <see cref="Packet"/> to the server.
        /// </summary>
        /// <param name="isFromServer">Whether or not the <see cref="Packet"/> originates from the server.</param>
        /// <param name="isResponse">Whether or not the <see cref="Packet"/> is a response to another one.</param>
        /// <param name="sequence">The sequence number of the <see cref="Packet"/>.</param>
        /// <param name="words">The <see cref="Packet">Packet's</see> words.</param>
        protected void SendPacket(bool isFromServer, bool isResponse, int sequence, params string[] words)
        {
            if (words.Length == 0) return;

            SendPacket(new Packet(isFromServer, isResponse, sequence, words));
        }

        /// <summary>
        /// Sends a response to a <see cref="Packet"/> originally sent by the server.
        /// </summary>
        /// <param name="sequence">The sequence number of the original <see cref="Packet"/>.</param>
        /// <param name="words">The words of the <see cref="Packet"/> being sent.</param>
        public void SendResponse(int sequence, params string[] words)
        {
            if (words.Length == 0) return;

            //not sure why isfromserver is true, but that's the way it is in python
            SendPacket(true, true, sequence, words);
        }

        /// <summary>
        /// Sends a request for information to the server.
        /// </summary>
        /// <param name="words">The words of the <see cref="Packet"/> being sent.</param>
        public void SendRequest(params string[] words)
        {
            if (words.Length == 0) return;

            SendPacket(false, false, RegisterSequence(words[0], false), words);
        }

        //used for tracking internal sequences
        internal void InternalSendRequest(params string[] words)
        {
            if (words.Length == 0) return;

            SendPacket(false, false, RegisterSequence(words[0], true), words);
        }

        int RegisterSequence(string word, bool isInternal)
        {
            int oldSequence;
            lock ("rsLock")
            {
                oldSequence = clientSequence;

                if (isInternal && SuppressInternalCommands && !internalSequences.Contains(clientSequence)) internalSequences.Add(clientSequence);

                sequenceTracker[clientSequence] = word;
                clientSequence = (clientSequence + 1) & 0x3fffffff; //increment the sequence
            }

            return oldSequence;
        }

        // to make thing simpler, two MRESs will be used instead of one
        // these are for LogOn
        ManualResetEventSlim getSalt = new ManualResetEventSlim(false);
        ManualResetEventSlim confirmLoggedOn = new ManualResetEventSlim(false);
        [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
        string tempPass = "";
        string salt = "";
        bool attemptingThreadedLogin = false;

        /// <summary>
        /// Logs the <see cref="RconClient"/> onto the server without blocking the thread. 
        /// </summary>
        /// <param name="password">The password for the server's RCON interface.</param>
        /// <remarks>If you'd like to block the thread while logging in, use <see cref="LogOn(string, bool)"/>
        /// <seealso cref="LogOn(string, bool)"/></remarks>
        public void LogOn(string password)
        {
            LogOn(password, true);
        }

        /// <summary>
        /// Logs the <see cref="RconClient"/> onto the server.
        /// </summary>
        /// <param name="password">The password for the server's RCON interface.</param>
        /// <param name="threaded">If true, the method will not block the thread, but you will not know if the login fails.</param>
        /// <returns>True if login was successful or false on a failed login or when <paramref name="threaded"/> is True.</returns>
        /// <remarks>
        /// In order to ensure this method doesn't block the thread when <paramref name="threaded"/> is false,
        /// each point of communication (there are two of them), will wait a period of time before returning false,
        /// as defined by <see cref="LogOnTimeout"/>. In the final point of communication, it is possible that
        /// the login attempt will succeed after timeout, so, while this method would return false, the <see cref="LoggedOn"/>
        /// event would still be raised.
        /// </remarks>
        public bool LogOn(string password, bool threaded)
        {
            if (!IsLoggedOn)
            {
                if (IsConnected && password != null && password.HasValue())
                {
                    if (threaded)
                    {
                        attemptingThreadedLogin = true;
                        tempPass = password;
                        InternalSendRequest("login.hashed");
                        return false;
                    }

                    //reset the MRESs
                    getSalt.Reset();
                    confirmLoggedOn.Reset();

                    InternalSendRequest("login.hashed");

                    if (!getSalt.Wait(LogOnTimeout * 1000)) return false; //timeout

                    //check for salt. if it gets this far, there should be no point in checking for it, tho
                    if (!salt.HasValue()) return false;

                    InternalSendRequest("login.hashed", GeneratePasswordHash(password, salt));

                    if (!confirmLoggedOn.Wait(LogOnTimeout * 1000)) return false;

                    return IsLoggedOn;

                }
                return false;
            }
            return true;
        }

        static string GeneratePasswordHash(string pass, string salt = "")
        {
            System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create();

            byte[] passBytes = Encoding.Default.GetBytes(pass);

            int saltByteCount = salt.Length / 2; //hex string
            byte[] allBytes = new byte[passBytes.Length + saltByteCount];

            for (int i = 0; i < salt.Length; i += 2)
                allBytes[i / 2] = Convert.ToByte(salt.Substring(i, 2), 16);//converts the hex value into a byte

            for (int i = 0; i < passBytes.Length; i++)
                allBytes[i + saltByteCount] = passBytes[i];

            string result = md5.ComputeHash(allBytes).ToHexString();
            md5.Dispose();

            return result.ToUpper(c);
        }

        static void PlayerRefreshCallback(object state)
        {
            RconClient client = state as RconClient;

            client.InternalSendRequest(client.playerRefreshCommand, "all");
        }

        #endregion

        #region Packet Handling
        static void HandlePacketReceive(object state)
        {
            ParsePacketState pState = (state as ParsePacketState);
            RconClient client = pState.Client;
            Packet packet = pState.Packet;

            Header header = packet.Header;
            ReadOnlyCollection<string> words = packet.Words;

            client.OnRawRead(new RawReadEventArgs(packet));

            if (header.IsResponse)
            {
                //although the logic is inefficient, it ensures the O(n) is used as little as possible
                if (!client.SuppressInternalCommands)
                    client.OnResponse(
                        new ResponseEventArgs(header.Sequence,
                            client.sequenceTracker.ContainsKey(header.Sequence) ? client.sequenceTracker[header.Sequence] : "",
                            words));
                else if (!client.internalSequences.Contains(header.Sequence))
                    client.OnResponse(
                        new ResponseEventArgs(header.Sequence,
                            client.sequenceTracker.ContainsKey(header.Sequence) ? client.sequenceTracker[header.Sequence] : "",
                            words));
                else client.internalSequences.Remove(header.Sequence);


                //handle response to commands
                if (client.sequenceTracker.ContainsKey(header.Sequence) && words.Count > 0)
                {
                    switch (client.sequenceTracker[header.Sequence])
                    {
                        case "login.hashed":
                            if (words.Count == 2) //means the response has the salt
                                if (client.attemptingThreadedLogin)
                                {
                                    client.InternalSendRequest("login.hashed", GeneratePasswordHash(client.tempPass, words[1]));
                                    client.tempPass = "";
                                    client.salt = "";
                                    client.attemptingThreadedLogin = false;
                                }
                                else
                                {
                                    client.salt = words[1];
                                    client.getSalt.Set();
                                }
                            else
                            {
                                client.IsLoggedOn = words[0] == "OK";

                                //send command to receive events, raise loggedon event, and change playerrefreshcommand
                                if (client.IsLoggedOn)
                                {
                                    client.OnLoggedOn();
                                    client.playerRefreshCommand = "admin.listPlayers";
                                    client.InternalSendRequest("admin.eventsEnabled", "true");
                                }

                                client.confirmLoggedOn.Set();
                            }
                            break;

                        case "logout":
                            client.playerRefreshCommand = "listPlayers";
                            break;

                        case "listPlayers":
                        case "admin.listPlayers":
                            if (words[0] == "OK")
                            {
                                int numberOfPlayers = Convert.ToInt32(words[1], c);
                                int numberOfParameters = Convert.ToInt32(words[2], c);

                                string[] parameterNames = new string[numberOfParameters];
                                for (int i = 0; i < numberOfParameters; i++)
                                    parameterNames[i] = words[i + 3];

                                int offset = numberOfParameters + 3; //to shorten code

                                for (int i = 0; i < numberOfPlayers; i++)
                                {
                                    //the name will be the 1st prop, so get it first
                                    string name = words[(i * numberOfParameters) + offset];

                                    //check to see if player exists
                                    if (client.Players.PlayerExists(name))
                                    {
                                        Player p = client.Players[name];

                                        //set properties of the existing player
                                        for (int j = 1; j < numberOfParameters; j++) //j=1 because name is already gotten
                                            p[parameterNames[j]] = words[(i * numberOfParameters) + offset + j];
                                    }
                                    else
                                    {
                                        Dictionary<string, string> playerProperties = new Dictionary<string, string>(numberOfParameters);

                                        //add name
                                        playerProperties["name"] = name;

                                        //add properties to dict
                                        for (int j = 1; j < numberOfParameters; j++) //j=1 because name is already gotten
                                            playerProperties[parameterNames[j]] = words[(i * numberOfParameters) + offset + j];

                                        Player newPlayer = new Player(client, playerProperties);

                                        client.Players.Add(newPlayer);

                                        //trigger the playerjoined event

                                        //this is done here because of the possibility the player update timer
                                        //will add the player before player.onjoin does, making this method more efficient
                                        client.OnPlayerJoined(new PlayerEventArgs(newPlayer));
                                    }
                                }
                            }
                            break; //listplayers

                    } //switch

                    //keep the dictionary from inflating
                    //since the server should respond to all requests, even bad ones, this dict shouldn't grow large
                    client.sequenceTracker.Remove(header.Sequence);
                }
            }
            else
            {
                //events, like player.join, will be caught here

                //get a player here. for player events, the the 1st word (zero-based index) will be a player name.
                //for non player events, the playercollection indexer will a player that wont be used
                //check to see that words[1] exists, tho
                Player p = words.Count > 1 && client.Players.PlayerExists(words[1]) ? client.Players[words[1]] : null;

                switch (words[0])
                {
                    case "player.left":
                        //raise the event before removing the player
                        client.OnPlayerLeft(new PlayerEventArgs(p));
                        client.Players.Remove(words[1]);
                        break;

                    case "player.onJoin":
                        //send a listplayers command and have the event trigger there
                        //this is done here because of the possibility the player update timer
                        //will add the player before player.onjoin does, making this method more efficient
                        client.SendRequest(client.playerRefreshCommand, "player", words[1]);
                        break;

                    case "player.onAuthenticated": //haven't seen this in the protocol, but it's in docs
                    case "player.authenticated":
                        //update the player
                        p.Guid = words[2];

                        client.OnPlayerAuthenticated(new PlayerAuthenticatedEventArgs(p, words[2]));
                        break;

                    case "player.killed":
                        Player victim = client.Players[words[2]];

                        client.OnPlayerKilled(new PlayerKilledEventArgs(victim, p, words[3], Convert.ToBoolean(words[4], c)));

                        //update players
                        p.Kills++;
                        victim.Deaths++;
                        break;

                    case "player.spawn":
                        client.OnPlayerSpawned(new PlayerEventArgs(p));
                        break;

                    case "player.onSquadChange":
                    case "player.switchTeam":
                        if (p == null) break;
                        //back up old team and squad, change values, then send event
                        int oldTeam = p.TeamId;
                        int oldSquad = p.SquadId;
                        int newTeam = Convert.ToInt32(words[2], c);
                        int newSquad = Convert.ToInt32(words[3], c);

                        p.TeamId = newTeam;
                        p.SquadId = newSquad;

                        client.OnPlayerMoved(new PlayerMovedEventArgs(p, oldTeam, oldSquad));
                        break;
                }

                //send the server an OK response. if the docs show a dif response is needed, will have to add logic
                client.SendResponse(packet.Header.Sequence, "OK");
            }

        }

        #endregion

        #region IDisposable Members
        private bool disposed = false;

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Disposes the RconClient object.
        /// </summary>
        /// <param name="disposing">Whether or not to release managed resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    _Socket.Close();
                    getSalt.Dispose();
                    confirmLoggedOn.Dispose();
                    playerRefreshTimer.Dispose();
                }
            }

            disposed = true;
        }

        #endregion
    }
}
